<?php
namespace modules\auth;
use \phiction\exceptions\login as login_required;
use \phiction\exceptions\denied as access_denied;
use \phiction\exceptions\access_control;
use \phiction\exceptions\bad_request;
use \phiction\array_utils as arr;

class api
{
    static function add_user($q, &$args)
    {
        $login = arr::get_or_bad_request($args, 'login');
        $pwd   = arr::get_or_bad_request($args, 'password');
        self::validate_with_role($q, $args, 'admin');

        if (storage::add_user($q, $login, $pwd)->run() !== 1)
            throw new bad_request("cannot add user");
    }

    static function reset_password($q, &$args)
    {
        $pwd = arr::get_or_bad_request($args, 'password');
        $shards = self::validate($q, $args);
        $id = $shards['id'];

        if (storage::reset_password($q, $id, $pwd)->run() !== 1)
            throw new bad_request("cannot reset password");
    }


    static function logout($q, &$args)
    {
        $args['token'] = '';
    }

    # authentication: login
    static function login($q, &$args)
    {
        $login = arr::get_or_bad_request($args, 'login');
        $pwd   = arr::get_or_bad_request($args, 'password');

        $user = storage::user_info_from_login($q, $login)->run();
        if (is_null($user)) throw new bad_request("no such user");

        $id   = $user['id'];
        $role = $user['role'];
        $seq  = $user['seq'];
        $credential = $user['credential'];

        if (!security::verify_password($pwd, $credential))
            throw new bad_request("incorrect password");

        $token = self::issue_token($id, $login, $role, $seq);
        $args['token'] = $token;
    }

    # authentication: validate
    static function validate($q, &$args): array
    {
        $token = arr::get_or_throw($args, 'token', new login_required());

        try {
            $shards = security::break_token($token);
        }
        catch (\Throwable $ex) {
            error_log("BEGIN BAD TOKEN\e[1;31m");
            error_log($ex);
            error_log("\e[0mEND BAD TOKEN");
            throw new login_required();
            // invalid token should just ask the user to login
        }

        $id      = arr::get_or_throw($shards, 'id',      new login_required());
        $seq     = arr::get_or_throw($shards, 'seq',     new login_required());
        $refresh = arr::get_or_throw($shards, 'refresh', new login_required());
        $login   = arr::get_or_throw($shards, 'login',   new login_required());
        $role    = arr::get_or_throw($shards, 'role',    new login_required());
        // as for why throw login_required:
        //   if the token is valid, but there are some fields missing,
        //   the server must have been updated, so just let the user
        //   re-login to obtain a new version of the token
        //   by throwing login_required.

        if (time() > $refresh) {     // validate by database required
            $user = storage::user_info_from_seq($q, $seq)->run();
            if (is_null($user)) throw new login_required();
            // "seq" not found in database means that
            // the token has been revoked. in that case,
            // just let the user re-login by throwing login_required.

            $token = self::issue_token($id, $login, $role, $seq);
            $args['token'] = $token;
        }

        return [
            'id' => $id,
            'login' => $login,
            'role' => $role,
        ];
    }

    // validate helper
    static function validate_with_role($q, &$args, string $expect_role): array
    {
        $shards = self::validate($q, $args);
        if ($shards['role'] !== $expect_role) throw new access_denied();
        return $shards;
    }

    // return null if not logged in
    // otherwise return shards
    static function login_status($q, &$args)
    {
        try {
            return self::validate($q, $args);
        }
        catch (access_control $ex) {
            return null;
        }
    }

    private static function issue_token(
        int $id,
        string $login,
        string $role,
        string $seq): string
    {
        return security::make_token([
            'id' => $id,
            'login' => $login,
            'role' => $role,
            'seq' => $seq,
        ]);
    }
}

